package com.swingson.properties;

import com.swingson.SwingsonGui;
import com.swingson.Utils;
import com.swingson.controller.Controller;
import com.swingson.exceptions.SwingsonException;
import com.swingson.jsonparser.JsonElement;
import com.swingson.other.ConditionAndAction;
import com.swingson.other.MethodExecutor;
import com.swingson.properties.impl.PropertyProcessors;
import java.awt.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JComponent;
import org.mvel2.MVEL;
import org.mvel2.ParserContext;
import org.mvel2.compiler.ExecutableStatement;

/**
 * @author sad
 */
public abstract class AbstractAssignMethodPropertyProcessor extends PropertyProcessors.PrimitivePropertyProcessor {

    protected abstract String getName();

    protected abstract void attachMethod(JsonElement jsonElement, JComponent component, SwingsonGui gui, String propertyName, MethodExecutor mExecutor, int methodArgumentVariant, ConditionAndAction conditionAndAction);

    protected abstract List<Class[]> methodArgumentsVariants();

    private class MethodExecutorWithVariant {

        MethodExecutor executor;
        int variant;

        public MethodExecutorWithVariant(MethodExecutor executor, int variant) {
            this.executor = executor;
            this.variant = variant;
        }

    }

    private MethodExecutorWithVariant findMethod(String methodName, JComponent component, SwingsonGui gui, String propertyName) {
        Method method = Utils.getMethod(gui.getControllerComponent().getClass(), methodName);
        boolean methodInMainController = false;
        if (method != null) {
            methodInMainController = true;
            //throw new SwingsonException("You try to attach " + getName() + " to component " + component.getClass().getSimpleName() + " with property '" + propertyName + "' but top compoment does not contain method " + methodName);
            MethodExecutor executor = new MethodExecutor(gui.getControllerComponent(), method);
            for (int i = 0; i < methodArgumentsVariants().size(); i++) {
                if (Utils.checkMethodParameters(method, methodName, methodArgumentsVariants().get(i))) {
                    return new MethodExecutorWithVariant(executor, i);
                }
            }
        }
        Controller findedController = null;
        for (Controller controller : gui.getControllers()) {
            method = Utils.getMethod(controller.getClass(), methodName);
            if (method != null) {
                findedController = controller;
                MethodExecutor executor = new MethodExecutor(controller, method);
                for (int i = 0; i < methodArgumentsVariants().size(); i++) {
                    if (Utils.checkMethodParameters(method, methodName, methodArgumentsVariants().get(i))) {
                        return new MethodExecutorWithVariant(executor, i);
                    }
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < methodArgumentsVariants().size(); i++) {
            if (i != 0) {
                sb.append(", ");
            }

            sb.append(Utils.classArrayToString(methodArgumentsVariants().get(i)));
        }

        sb.append("]");
        if (methodInMainController) {
            throw new SwingsonException("Method " + methodName + " in class " + gui.getControllerComponent().getClass().getSimpleName()
                    + " for action event " + propertyName + " should have one from the following parameters "
                    + sb.toString());
        } else if (findedController != null) {
            throw new SwingsonException("Method " + methodName + " in controller class " + findedController.getClass().getSimpleName()
                    + " for action event " + propertyName + " should have one from the following parameters "
                    + sb.toString());
        } else {
            throw new SwingsonException("Method '" + methodName + "' not found in class '" + gui.getControllerComponent().getClass().getSimpleName()
                    + "' and in all nested controller classes for action event '" + propertyName + "'");
        }
    }

    private static class MethodAndScript {
        String method;
        String script;

        public MethodAndScript(String method, String script) {
            this.method = method;
            this.script = script;
        }

        @Override
        public String toString() {
            return "Method=" + method + "; Script={" + script + "}";
        }
    }

    private static Pattern methodPattern = Pattern.compile("(^\\s*(?<method1>[^\\{\\}]*?)\\s*(\\{(?<script1>.*?)\\})?\\s*$)|(^\\s*(\\{(?<script2>.*?)\\})?\\s*(?<method2>[^\\{\\}]*?)\\s*$)");

    public static String firstNotNull(String... values) {
        for (String value : values) {
            if (value != null && !value.trim().isEmpty()) {
                return value.trim();
            }
        }
        return null;
    }

    private MethodAndScript parseArgument(String value) {
        Matcher matcher = methodPattern.matcher(value);
        if (matcher.matches()) {
            String method = firstNotNull(matcher.group("method1"), matcher.group("method2"));
            String script = firstNotNull(matcher.group("script1"), matcher.group("script2"));
            return new MethodAndScript(method, script);
        }
        return null;
    }

    @Override
    protected Object getValue(JsonElement jsonElement, JComponent component, SwingsonGui gui, String propertyName) {
        String value = jsonElement.getAsString();
        MethodAndScript parsedValue = parseArgument(value);
        if (parsedValue == null) {
            throw new SwingsonException("Cannot parse value for property " + propertyName + " for component " + component + " with value [" + value + "]. Should be on of the: \"method\",\"method{script}\",\"{script}\",\"{script}method\"");
        }

        ConditionAndAction conditionAndAction = null;
        if (parsedValue.script != null) {
            String scriptText = parsedValue.script;
            ParserContext context = new ParserContext();
            MVEL.analyze(scriptText, context);
            Map<String, Class> inputsMap = context.getInputs();
            List<String> inputsList = new ArrayList<>(inputsMap.keySet());
            ExecutableStatement compiledExpression = (ExecutableStatement) MVEL.compileExpression(scriptText);
            conditionAndAction = new ConditionAndAction(compiledExpression, null);
            conditionAndAction.addInputVariables(inputsList);
        }

        if (parsedValue.method != null) {
            MethodExecutorWithVariant executor = findMethod(parsedValue.method, component, gui, propertyName);
            if (executor != null) {
                attachMethod(jsonElement, component, gui, propertyName, executor.executor, executor.variant, conditionAndAction);
            }
        } else {
            MethodExecutor emptyExecutor = new MethodExecutor(null, null);
            attachMethod(jsonElement, component, gui, propertyName, emptyExecutor, 0, conditionAndAction);
        }

        return null;
    }

    protected void runAction(ConditionAndAction conditionAndAction, SwingsonGui gui, Component component) {
        if (conditionAndAction != null) {
            gui.runConditionAndAction(conditionAndAction, component);
        }
    }
}
